/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.IO.IsolatedStorage;
using System.Xml;
using System.Runtime.Serialization;
using System.Globalization;

namespace DaveSexton.DocProject
{
	[Serializable]
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2240:ImplementISerializableCorrectly")]
	public class IsolatedSettingsDictionary<TKey, TValue> : BufferedSettingsDictionary<TKey, TValue>, IDisposable
	{
		#region Public Properties
		public string File
		{
			get
			{
				return file;
			}
		}

		public IsolatedStorageFile Storage
		{
			get
			{
				return storage;
			}
		}

		public override bool CanCommit
		{
			get
			{
				return true;
			}
		}

		public bool CommitOnSave
		{
			get
			{
				return commitOnSave;
			}
			set
			{
				commitOnSave = value;
			}
		}
		#endregion

		#region Private / Protected
		private readonly string file;
		private readonly IsolatedStorageFile storage;
		private readonly XmlDocument document = new XmlDocument();
		private readonly XmlNode root;
		private bool commitOnSave = true;
		#endregion

		#region Constructors
		#region Type (auto-generated file name)
		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.IsolatedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor(System.IO.IsolatedStorage.IsolatedStorageFile,System.String)")]
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type)
			: this(storage, CreateSettingsFileNameForType(type))
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, int capacity)
			: this(storage, CreateSettingsFileNameForType(type), capacity)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, int capacity, IEqualityComparer<TKey> comparer)
			: this(storage, CreateSettingsFileNameForType(type), capacity, comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, IEqualityComparer<TKey> comparer)
			: this(storage, CreateSettingsFileNameForType(type), comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.IsolatedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor(System.String)")]
		public IsolatedSettingsDictionary(Type type)
			: this(CreateSettingsFileNameForType(type))
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, int capacity)
			: this(CreateSettingsFileNameForType(type), capacity)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, int capacity, IEqualityComparer<TKey> comparer)
			: this(CreateSettingsFileNameForType(type), capacity, comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, IEqualityComparer<TKey> comparer)
			: this(CreateSettingsFileNameForType(type), comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, CultureInfo culture)
			: this(storage, CreateSettingsFileNameForType(type), culture)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, CultureInfo culture, int capacity)
			: this(storage, CreateSettingsFileNameForType(type), culture, capacity)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, CultureInfo culture, int capacity, IEqualityComparer<TKey> comparer)
			: this(storage, CreateSettingsFileNameForType(type), culture, capacity, comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, Type type, CultureInfo culture, IEqualityComparer<TKey> comparer)
			: this(storage, CreateSettingsFileNameForType(type), culture, comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, CultureInfo culture)
			: this(CreateSettingsFileNameForType(type), culture)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, CultureInfo culture, int capacity)
			: this(CreateSettingsFileNameForType(type), culture, capacity)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, CultureInfo culture, int capacity, IEqualityComparer<TKey> comparer)
			: this(CreateSettingsFileNameForType(type), culture, capacity, comparer)
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		public IsolatedSettingsDictionary(Type type, CultureInfo culture, IEqualityComparer<TKey> comparer)
			: this(CreateSettingsFileNameForType(type), culture, comparer)
		{
		}
		#endregion

		#region File string
		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, int capacity)
			: base(capacity)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, int capacity, IEqualityComparer<TKey> comparer)
			: base(capacity, comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, IEqualityComparer<TKey> comparer)
			: base(comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(string file)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(string file, int capacity)
			: base(capacity)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(string file, int capacity, IEqualityComparer<TKey> comparer)
			: base(capacity, comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "DaveSexton.DocProject.BufferedSettingsDictionary`2<type parameter.TKey,type parameter.TValue>.#ctor")]
		public IsolatedSettingsDictionary(string file, IEqualityComparer<TKey> comparer)
			: base(comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, CultureInfo culture)
			: base(culture)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, CultureInfo culture, int capacity)
			: base(culture, capacity)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, CultureInfo culture, int capacity, IEqualityComparer<TKey> comparer)
			: base(culture, capacity, comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated <paramref name="storage"/> 
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(IsolatedStorageFile storage, string file, CultureInfo culture, IEqualityComparer<TKey> comparer)
			: base(culture, comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			if (storage == null)
				throw new ArgumentNullException("storage");

			this.file = file;
			this.storage = storage;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(string file, CultureInfo culture)
			: base(culture)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(string file, CultureInfo culture, int capacity)
			: base(culture, capacity)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(string file, CultureInfo culture, int capacity, IEqualityComparer<TKey> comparer)
			: base(culture, capacity, comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <param name="file">Relative path and file name of an xml document in isolated storage
		/// that will be used to persist settings.</param>
		public IsolatedSettingsDictionary(string file, CultureInfo culture, IEqualityComparer<TKey> comparer)
			: base(culture, comparer)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			this.file = file;

			root = LoadSettings();
		}
		#endregion

		/// <summary>
		/// Constructs a new instance of the <see cref="IsolatedSettingsDictionary" /> class for deserialization.
		/// </summary>
		protected IsolatedSettingsDictionary(SerializationInfo info, StreamingContext context)
			: base(info, context)
		{
		}
		#endregion

		#region Methods
		private static string CreateSettingsFileNameForType(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");

			string name = type.AssemblyQualifiedName;

			using (System.Security.Cryptography.SHA1Managed sha1 = new System.Security.Cryptography.SHA1Managed())
			{
				byte[] hash = sha1.ComputeHash(System.Text.Encoding.Unicode.GetBytes(name));

				string prefix = Convert.ToBase64String(hash, Base64FormattingOptions.None);

				foreach (char c in System.IO.Path.GetInvalidFileNameChars())
					prefix = prefix.Replace(c, '_');

				return prefix + "_settings.xml";
			}
		}

		private static XmlNode CreateEmptySettingsDocument(XmlDocument document)
		{
			document.AppendChild(document.CreateXmlDeclaration("1.0", null, null));

			return CreateEmptySettingsNode(document);
		}

		private static XmlNode CreateEmptySettingsNode(XmlDocument document)
		{
			return document.AppendChild(document.CreateElement("", "settings", ""));
		}

		[System.Diagnostics.DebuggerHidden]
		public virtual IsolatedStorageFileStream GetStream(string path, FileMode mode)
		{
			FileAccess access;
			FileShare share;

			if (mode == FileMode.Open || mode == FileMode.OpenOrCreate)
			{
				access = FileAccess.Read;
				share = FileShare.Read;
			}
			else
			{
				access = FileAccess.Write;
				share = FileShare.None;
			}

			return (storage == null)
				? new IsolatedStorageFileStream(path, mode, access, share)
				: new IsolatedStorageFileStream(path, mode, access, share, storage);
		}

		[System.Diagnostics.DebuggerHidden]
		private XmlNode LoadSettings()
		{
			document.PreserveWhitespace = true;

			XmlNode settings;

			try
			{
				using (IsolatedStorageFileStream stream = GetStream(file, FileMode.Open))
				{
					document.Load(stream);

					settings = document.DocumentElement ?? CreateEmptySettingsNode(document);
				}
			}
			catch (XmlException ex)
			{
				Log.Exception(ex, "Error occurred while loading an XML settings document: {0}", file);
				
				settings = CreateEmptySettingsDocument(document);
			}
			catch (DirectoryNotFoundException)
			{
				settings = CreateEmptySettingsDocument(document);
			}
			catch (FileNotFoundException)
			{
				settings = CreateEmptySettingsDocument(document);
			}

			foreach (XmlNode item in settings)
			{
				if (item.NodeType != XmlNodeType.Element)
					continue;

				XmlAttribute attribute = item.Attributes["name", ""];

				if (attribute != null)
				{
					string key = attribute.Value;
					string value = item.InnerText;

					CommitDictionary.Add(ConvertType<string, TKey>(key, Culture), ConvertType<string, TValue>(value, Culture));
				}
			}

			return settings;
		}

		public void Save()
		{
			Save(file);
		}

		/// <param name="file">Relative path and file name of an XML document in isolated storage
		/// that will be used to persist the current settings.</param>
		public virtual void Save(string path)
		{
			if (commitOnSave)
				Commit();

			root.RemoveAll();

			List<string> keys = new List<string>(Count);

			foreach (KeyValuePair<TKey, TValue> pair in CommitDictionary)
			{
				string key = ConvertType<TKey, string>(pair.Key, Culture);

				XmlNode item;

				if (!keys.Exists(delegate(string k) { return string.Compare(k, key, true, Culture) == 0; }))
				{
					keys.Add(key);

					item = root.AppendChild(document.CreateElement("", "item", ""));
					XmlAttribute name = item.Attributes.Append(document.CreateAttribute("", "name", ""));

					name.Value = key;
				}
				else
					item = root.SelectSingleNode("item[@name=\"" + key.Replace("\"", "&quot;") + "\"]");

				item.InnerText = ConvertType<TValue, string>(pair.Value, Culture);
			}

			using (IsolatedStorageFileStream stream = GetStream(path, FileMode.Create))
			{
				XmlWriterSettings writerSettings = new XmlWriterSettings();
				writerSettings.Indent = true;

				using (XmlWriter writer = XmlWriter.Create(stream, writerSettings))
				{
					document.Save(writer);
				}
			}
		}
		#endregion

		#region IDisposable Members
		/// <summary>
		/// Releases all resources used by an instance of the <see cref="IsolatedSettingsDictionary" /> class.
		/// </summary>
		/// <remarks>
		/// This method calls the virtual <see cref="Dispose(bool)" /> method, passing in <strong>true</strong>, and then suppresses 
		/// finalization of the instance.
		/// </remarks>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Releases unmanaged resources before an instance of the <see cref="IsolatedSettingsDictionary" /> class is reclaimed by garbage collection.
		/// </summary>
		/// <remarks>
		/// This method releases unmanaged resources by calling the virtual <see cref="Dispose(bool)" /> method, passing in <strong>false</strong>.
		/// </remarks>
		~IsolatedSettingsDictionary()
		{
			Dispose(false);
		}

		/// <summary>
		/// Releases the unmanaged resources used by an instance of the <see cref="IsolatedSettingsDictionary" /> class and optionally releases the managed resources.
		/// </summary>
		/// <param name="disposing"><strong>true</strong> to release both managed and unmanaged resources; <strong>false</strong> to release only unmanaged resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if (disposing && storage != null)
				storage.Dispose();
		}
		#endregion
	}
}
